package uz.rt.units;

/** The class defines an angle depending on the unit by which it is defined */
public class Angle implements Comparable<Angle>
{
// ------------------------------ FIELDS ------------------------------

    /** Defines the lower limit of the angle */
    private double lowerLimit = -180.0;
    /** Defines the upper limit of the angle */
    private double upperLimit = 180.0;
    /** The default value the middle between the lower and upper limit */
    private double value = (lowerLimit + upperLimit) / 2;
    /** The unit in which the angle is defines (either degrees or radians). */
    private Unit unit;

// --------------------------- CONSTRUCTORS ---------------------------

    /**
     * Constructor intialises the default value and sets the unit.
     *
     * @param unit Angular unit
     */
    public Angle(final Unit unit)
    {
        if (unit == Unit.radian)
        {
            scale(unit);
        }
    }

    /**
     * Scale the magnitude of the angle based on a unit
     * Current support conversion are:
     * <ul>
     * <li>degree to radian</li>
     * <li>radian to degree</li>
     * </ul>
     *
     * @param unit Angle unit
     */
    public void scale(Unit unit)
    {
        switch (unit)
        {
            case degree:
                if (this.unit == Unit.radian)
                {
                    lowerLimit = radian2degree(lowerLimit);
                    upperLimit = radian2degree(upperLimit);
                    setValue(radian2degree(value));
                }
                this.unit = unit;
                break;
            case radian:
                if (this.unit == Unit.degree)
                {
                    lowerLimit = degree2radian(lowerLimit);
                    upperLimit = degree2radian(upperLimit);
                    setValue(degree2radian(value));
                }
                this.unit = unit;
                break;
            default:
                break;
        }
    }

    /**
     * Convert radians to degrees
     *
     * @param x Angular value in radians
     * @return Angular value in degrees
     */
    public static double radian2degree(final double x)
    {
        return x * 180 / Math.PI;
    }

    /**
     * Convert degrees into radians
     *
     * @param x Angular value in degrees
     * @return Angular value in radians
     */
    public static double degree2radian(final double x)
    {
        return x * Math.PI / 180;
    }

    /**
     * Constructor intialises the angle to a value and sets the unit.
     *
     * @param a    Value of the angle
     * @param unit Angular unit
     */
    public Angle(final double a, final Unit unit)
    {
        if (unit == Unit.radian)
        {
            setLowerLimit(-1 * Math.PI);
            setUpperLimit(Math.PI);
        }
        setValue(a);
        setUnit(unit);
    }

    /**
     * Constructor intialises the angle to a value, bounded by it's limits and sets the unit.
     *
     * @param a          Value of the angle
     * @param lowerLimit Lower anglular limit
     * @param upperLimit Upper angular limit
     * @param unit       Angular unit
     */
    public Angle(final double a, final double lowerLimit, final double upperLimit, final Unit unit)
    {
        setLowerLimit(lowerLimit);
        setUpperLimit(upperLimit);
        setValue(a);
        setUnit(unit);
    }

// --------------------- GETTER / SETTER METHODS ---------------------

    /**
     * Get the lower limit
     *
     * @return Lower limit
     */
    public double getLowerLimit()
    {
        return lowerLimit;
    }

    /**
     * Set the lower limit
     *
     * @param lowerLimit Lower limit
     */
    public void setLowerLimit(double lowerLimit)
    {
        this.lowerLimit = lowerLimit;
    }

    /**
     * Get the angle unit
     *
     * @return Angle unit
     */
    public Unit getUnit()
    {
        return unit;
    }

    /**
     * Set the angle unit
     *
     * @param unit Angle unit
     */
    public void setUnit(Unit unit)
    {
        this.unit = unit;
    }

    /**
     * Get the upper limit
     *
     * @return Upper limit
     */
    public double getUpperLimit()
    {
        return upperLimit;
    }

    /**
     * Set the upper limit
     *
     * @param upperLimit Upper limit
     */
    public void setUpperLimit(double upperLimit)
    {
        this.upperLimit = upperLimit;
    }

    /**
     * Get the anglular value
     *
     * @return Angular value
     */
    public double getValue()
    {
        return value;
    }

    /**
     * Set the anglular value
     *
     * @param value Angular value
     */
    public void setValue(double value)
    {
        this.value = value;
    }

// ------------------------ CANONICAL METHODS ------------------------

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (!(o instanceof Angle)) return false;

        Angle angle = (Angle) o;

        return Double.compare(angle.lowerLimit, lowerLimit) == 0 && Double.compare(angle.upperLimit, upperLimit) == 0
                && Double.compare(angle.value, value) == 0;
    }

    @Override
    public int hashCode()
    {
        int result;
        long temp;
        temp = lowerLimit != +0.0d ? Double.doubleToLongBits(lowerLimit) : 0L;
        result = (int) (temp ^ (temp >>> 32));
        temp = upperLimit != +0.0d ? Double.doubleToLongBits(upperLimit) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        temp = value != +0.0d ? Double.doubleToLongBits(value) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    @Override
    public String toString()
    {
        String v = String.valueOf(value) + " ";
        if (unit == Unit.degree) return v + "degrees";
        if (unit == Unit.radian) return v + "radians";
        return "";
    }

// ------------------------ INTERFACE METHODS ------------------------


// --------------------- Interface Comparable ---------------------

    public int compareTo(Angle o)
    {
        return Double.compare(value, o.value);
    }

// -------------------------- OTHER METHODS --------------------------

    /**
     * Checks if the angluar value exceeds the upper and lower limits
     *
     * @return True if the angular value is within limits
     */
    public boolean isValid()
    {
        return value >= lowerLimit && value <= upperLimit && lowerLimit <= upperLimit;
    }

    /**
     * Returns a scaled angular value depending on the requested unit
     *
     * @param unit Angular unit
     * @return Scaled angular value
     */
    public double scaled(Unit unit)
    {
        Angle angle = new Angle(value, this.unit);
        angle.scale(unit);
        return angle.getValue();
    }

// -------------------------- ENUMERATIONS --------------------------

    /**
     * Defines the different length units supported by this class:
     * <ul>
     * <li>degree</li>
     * <li>radian</li>
     * </ul>
     */
    public enum Unit
    {
        degree, radian
    }
}
